package com.hzw.saas.service.old.hub.service.impl;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.util.List;
import java.util.Objects;

import com.hzw.saas.common.util.enums.ConflictModeEnum;
import com.hzw.saas.service.old.hub.enums.LocChunkStatusEnum;
import com.hzw.saas.service.old.hub.enums.PathType;
import com.hzw.saas.service.old.hub.pojo.FileStorage;
import com.hzw.saas.service.old.hub.pojo.InxChunkMetadata;
import com.hzw.saas.service.old.hub.pojo.InxChunkStorage;
import com.hzw.saas.service.old.hub.pojo.LocChunkStorage;
import com.hzw.saas.service.old.hub.service.IChunkStorageService;
import com.hzw.saas.service.old.hub.service.IFileStorageService;
import com.hzw.saas.service.old.hub.util.StorageUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.SystemClock;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * 分片存储默认实现
 *
 * @author zzl
 * @since 04/21/2021
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class OldChunkStorageServiceImpl implements IChunkStorageService {

    private final IFileStorageService fileStorageService;

    /**
     * 分片元数据信息，用于索引类型分片保存分片总数等
     */
    public static final String METADATA_FILE_NAME = "metadata";
    private static final String TMP_PREFIX = ".tmp";

    private File getChunkParent(String fileId, String... superPaths) {
        Assert.isTrue(StrUtil.isNotBlank(fileId), "fileId is blank");
        String superPath = StorageUtil.connectPath(superPaths);
        String chunkPath = StorageUtil.connectPath(superPath, fileId);
        return FileUtil.file(chunkPath);
    }

    @Override
    public File getChunk(String fileId, String chunkId, String... superPaths) {
        Assert.isTrue(StrUtil.isNotBlank(chunkId), "chunkId is blank");
        File parent = this.getChunkParent(fileId, superPaths);
        return FileUtil.file(parent, chunkId);
    }

    @Override
    public boolean existsChunk(String fileId, String chunkId, String... superPaths) {
        File chunk = this.getChunk(fileId, chunkId, superPaths);
        return FileUtil.exist(chunk);
    }

    @Override
    public long getChunkLength(String fileId, String chunkId, String... superPaths) {
        File chunk = this.getChunk(fileId, chunkId, superPaths);
        return StorageUtil.getFileLength(chunk);
    }

    @Override
    public String getChunkDigest(String digestType, String fileId, String chunkId, String... superPaths) {
        File chunk = this.getChunk(fileId, chunkId, superPaths);
        return StorageUtil.getFileDigest(chunk, digestType);
    }


    @Override
    public void saveChunk(LocChunkStorage chunkInfo, byte[] chunkBytes) {
        // 参数校验
        Assert.notNull(chunkBytes, "save loc chunk: data to save is null.");
        Assert.notNull(chunkInfo, "save loc chunk: chunk info is null");
        // 校验源
        StorageUtil.checkSrcLength(chunkBytes, chunkInfo.getLength());
        StorageUtil.checkSrcDigest(chunkBytes, chunkInfo.getDigest(), chunkInfo.getDigestType());
        File chunk = this.getChunk(chunkInfo.getFileId(), chunkInfo.getChunkId(), chunkInfo.getSuperPath());
        String chunkInfoStr = JSONUtil.toJsonStr(chunkInfo);
        // 保存
        log.debug("save the loc chunk: {}", chunkInfoStr);
        long start = SystemClock.now();
        FileUtil.writeBytes(chunkBytes, chunk);
        long costTime = SystemClock.now() - start;
        // 校验目标
        Long length = chunkInfo.getStartBytes() != null && chunkInfo.getEndBytes() != null ?
            chunkInfo.getEndBytes() - chunkInfo.getStartBytes() + 1 : chunkInfo.getLength();
        StorageUtil.checkTarLength(chunk, length, true);
        StorageUtil.checkTarDigest(chunk, chunkInfo.getDigest(), chunkInfo.getDigestType(), true);
        log.info("save the loc chunk completed (cost: {}ms): {}", costTime, chunkInfoStr);
    }

    @Override
    public void saveChunk(InxChunkStorage chunkInfo, byte[] chunkBytes) {
        // 参数校验
        Assert.notNull(chunkBytes, "save inx chunk: data to save is null.");
        Assert.notNull(chunkInfo, "save inx chunk: chunk info is null");
        Assert.isTrue(!ObjectUtil.hasNull(chunkInfo.getChunkNumber(), chunkInfo.getCurrentChunkSize(), chunkInfo.getTotalChunks()),
            "save inx chunk: chunk params has null");
        // 校验源
        StorageUtil.checkSrcLength(chunkBytes, chunkInfo.getCurrentChunkSize());
        StorageUtil.checkSrcDigest(chunkBytes, chunkInfo.getDigest(), chunkInfo.getDigestType());
        // 保存元数据
        if (Objects.equals(chunkInfo.getChunkNumber(), 1) ||
            (ObjectUtil.compare(chunkInfo.getTotalChunks(), 10) > 0 && Objects.equals(chunkInfo.getChunkNumber(), chunkInfo.getTotalChunks()))) {
            this.writeMetadata(chunkInfo);
        }
        // 保存分片数据
        File chunk = this.getChunk(chunkInfo.getFileId(), String.valueOf(chunkInfo.getChunkNumber()), chunkInfo.getSuperPath());
        String chunkInfoStr = JSONUtil.toJsonStr(chunkInfo);
        log.debug("save the inx chunk: {}", chunkInfoStr);
        long start = SystemClock.now();
        FileUtil.writeBytes(chunkBytes, chunk);
        long costTime = SystemClock.now() - start;
        // 校验目标
        StorageUtil.checkTarLength(chunk, chunkInfo.getCurrentChunkSize(), true);
        StorageUtil.checkTarDigest(chunk, chunkInfo.getDigest(), chunkInfo.getDigestType(), true);
        log.info("save the inx chunk completed (cost: {}ms): {}", costTime, chunkInfoStr);

    }

    @Override
    public void deleteChunk(String fileId, String chunkId, String... superPaths) {
        File chunk = this.getChunk(fileId, chunkId, superPaths);
        log.debug("delete chunk: {}", chunk);
        FileUtil.del(chunk);
        log.info("delete chunk completed: {}", chunk);
    }

    @Override
    public void deleteChunks(String fileId, String... superPaths) {
        File chunksFolder = this.getChunkParent(fileId, superPaths);
        log.debug("delete chunks: {}", chunksFolder);
        FileUtil.del(chunksFolder);
        log.info("delete chunks completed: {}", chunksFolder);
    }

    @Override
    public void writeChunkMetadata(InxChunkMetadata inxChunkMetadata, String fileId, String... superPaths) {
        File file = FileUtil.file(this.getChunkParent(fileId, superPaths), METADATA_FILE_NAME);
        String metadata = JSONUtil.toJsonStr(inxChunkMetadata);
        try {
            log.debug("写入分片元数据: {}", metadata);
            FileUtil.writeUtf8String(metadata, file);
            log.info("写入分片元数据完成: {}", metadata);
        } catch (Exception e) {
            throw new IORuntimeException(e, "写入分片元数据异常: {}", metadata);
        }
    }

    @Override
    public InxChunkMetadata readChunkMetadata(String fileId, String... superPaths) {
        File file = FileUtil.file(this.getChunkParent(fileId, superPaths), METADATA_FILE_NAME);
        if (!FileUtil.exist(file))
            return null;
        try {
            String metadata = FileUtil.readUtf8String(file);
            return JSONUtil.toBean(metadata, InxChunkMetadata.class);
        } catch (Exception e) {
            throw new IORuntimeException(e, "读取分片元数据异常: {}", fileId);
        }
    }

    @Override
    public File mergeFileChunks(FileStorage fileInfo, String chunkSuperPath, String fileId, List<LocChunkStorage> chunkInfoList, boolean delChunks, ConflictModeEnum mode) throws IORuntimeException {
        Assert.notNull(fileInfo, "merge loc chunks: file info is null");
        log.debug("merge loc file({}) chunks...", fileId);
        // 合并前检查
        this.mergeCheck(chunkSuperPath, fileId);
        this.checkLocChunks(1, fileInfo.length(), chunkInfoList);
        // 目标文件重名检查
        File tarFile = this.fileStorageService.getFile(fileInfo.getFileName(), fileInfo.getSuperPath());
        tarFile = StorageUtil.doConflictName(tarFile, mode, PathType.FILE, false);
        fileInfo.setFileName(tarFile.getName());
        // 可能存在的已合并的文件
        File file = this.fileStorageService.getFile(fileInfo.getFileName(), fileInfo.getSuperPath());
        // 合并分片
        File tmpFile = this.touchTmpFile(chunkSuperPath, fileId);
        long costTime = this.mergeLocChunks(chunkSuperPath, fileId, chunkInfoList, tmpFile, file);
        // 校验文件
        StorageUtil.checkTarLength(tmpFile, fileInfo.getLength(), true);
        StorageUtil.checkTarDigest(tmpFile, fileInfo.getDigest(), fileInfo.getDigestType(), true);
        // 完成合并
        tarFile = this.renameTmpFile(chunkSuperPath, fileId, fileInfo.getSuperPath(), fileInfo.getFileName(), mode);
        fileInfo.setFileName(tarFile.getName());
        log.info("合并loc文件分片({})完成，耗时 {}ms", fileId, costTime);
        // 合并完成，删除分片
        this.deleteChunksAfterMerged(chunkSuperPath, fileId, delChunks);
        return tarFile;
    }

    @Override
    public File appendFileChunks(FileStorage fileInfo, String chunkSuperPath, String fileId, List<LocChunkStorage> chunkInfoList, boolean delChunks, ConflictModeEnum mode) throws IORuntimeException {
        Assert.notNull(fileInfo, "append loc chunks: file info is null");
        log.debug("append loc file({}) chunks...", fileId);
        return null;
    }

    @Override
    public File mergeFileChunks(FileStorage fileInfo, String chunkSuperPath, String fileId, boolean delChunks, ConflictModeEnum mode) throws IORuntimeException {
        Assert.notNull(fileInfo, "merge inx chunks: file info is null");
        log.debug("merge inx file({}) chunks...", fileId);
        // 合并前检查
        this.mergeCheck(chunkSuperPath, fileId);
        InxChunkMetadata chunkMetadata = this.checkInxChunks(fileInfo, chunkSuperPath, fileId);
        // 目标文件重名检查
        File tarFile = this.fileStorageService.getFile(chunkMetadata.getFileName(), chunkMetadata.getSuperPath());
        tarFile = StorageUtil.doConflictName(tarFile, mode, PathType.FILE, false);
        fileInfo.setFileName(tarFile.getName());
        // 合并分片
        File tmpFile = this.touchTmpFile(chunkSuperPath, fileId);
        long costTime = this.mergeInxChunks(chunkSuperPath, fileId, chunkMetadata.getTotalChunks(), tmpFile);
        // 校验文件
        StorageUtil.checkTarLength(tmpFile, chunkMetadata.getLength(), true);
        StorageUtil.checkTarDigest(tmpFile, chunkMetadata.getDigest(), chunkMetadata.getDigestType(), true);
        // 完成合并
        tarFile = this.renameTmpFile(chunkSuperPath, fileId, chunkMetadata.getSuperPath(), chunkMetadata.getFileName(), mode);
        fileInfo.setFileName(tarFile.getName());
        log.info("合并inx文件分片({})完成，耗时 {}ms", fileId, costTime);
        this.deleteChunksAfterMerged(chunkSuperPath, fileId, delChunks);
        return tarFile;
    }

    private void mergeCheck(String chunkSuperPath, String fileId) {
        // 分片目录检查
        File chunkParent = this.getChunkParent(fileId, chunkSuperPath);
        if (!chunkParent.isDirectory())
            throw new IORuntimeException(new FileNotFoundException("chunks folder not found：" + fileId));
    }

    private long mergeLocChunks(String chunkSuperPath, String fileId, List<LocChunkStorage> chunkInfoList, File tmpFile, File tarFile) {
        // 计时开始
        long start = SystemClock.now();
        try (
            RandomAccessFile fileRa = tarFile.exists() ? new RandomAccessFile(tarFile, "r") : null;
            OutputStream out = FileUtil.getOutputStream(tmpFile)
        ) {
            for (LocChunkStorage chunkInfo : chunkInfoList) {
                byte[] chunkBytes = new byte[0];
                if (ObjectUtil.equal(chunkInfo.getStatus(), LocChunkStatusEnum.MERGING.code())) {
                    File chunk = this.getChunk(fileId, chunkInfo.getChunkId(), chunkSuperPath);
                    chunkBytes = FileUtil.readBytes(chunk);
                }
                if (ObjectUtil.equal(chunkInfo.getStatus(), LocChunkStatusEnum.MERGED.code())) {
                    if (fileRa == null)
                        throw new IORuntimeException(new FileNotFoundException("原文件不存在：" + fileId));
                    int offset = chunkInfo.getStartBytes().intValue() - 1;
                    int length = (int) (chunkInfo.getEndBytes() - chunkInfo.getStartBytes()) + 1;
                    chunkBytes = new byte[length];
                    fileRa.read(chunkBytes, offset, length);
                }
                IoUtil.write(out, false, chunkBytes);
            }
            IoUtil.flush(out);
        } catch (IORuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new IORuntimeException(e);
        }
        // 计时结束
        return SystemClock.now() - start;
    }

    /**
     * 索引型分片合并
     *
     * @param chunkSuperPath
     * @param fileId
     * @param totalChunks
     * @return
     */
    private long mergeInxChunks(String chunkSuperPath, String fileId, int totalChunks, File tmpFile) {
        // 计时开始
        long start = SystemClock.now();
        try (
            OutputStream out = FileUtil.getOutputStream(tmpFile)
        ) {
            for (int i = 1; i <= totalChunks; i++) {
                log.debug("merge file chunk：No.{}...", i);
                File chunk = this.getChunk(fileId, String.valueOf(i), chunkSuperPath);
                if (!FileUtil.exist(chunk)) {
                    String errorMsg = String.format("file(%s) chunk not found: No.%s", fileId, i);
                    throw new IORuntimeException(new FileNotFoundException(errorMsg));
                }
                IoUtil.write(out, false, FileUtil.readBytes(chunk));
            }
            IoUtil.flush(out);
        } catch (IORuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new IORuntimeException(e);
        }
        // 计时结束
        return SystemClock.now() - start;
    }

    private void deleteChunksAfterMerged(String chunkSuperPath, String fileId, boolean delChunks) {
        // 合并完成，删除分片
        if (delChunks) {
            try {
                this.deleteChunks(fileId, chunkSuperPath);
            } catch (Exception ignored) {}
        }
    }

    /*************************************************合并临时文件工具**************************************************/

    private File getTmpFile(String chunkSuperPath, String fileId) {
        return FileUtil.file(chunkSuperPath, fileId, fileId + TMP_PREFIX);
    }

    private File touchTmpFile(String chunkSuperPath, String fileId) {
        File tmpFile = this.getTmpFile(chunkSuperPath, fileId);
        log.debug("创建分片合并临时文件: {}", tmpFile);
        return FileUtil.touch(tmpFile);
    }

    private File renameTmpFile(String chunkSuperPath, String fileId, String fileSuperPath, String fileName, ConflictModeEnum mode) {
        File tmpFile = this.getTmpFile(chunkSuperPath, fileId);
        if (!tmpFile.exists())
            throw new IORuntimeException(new FileNotFoundException("分片合并临时文件丢失: " + tmpFile.toString()));
        File tarFile = this.fileStorageService.getFile(fileName, fileSuperPath);
        return this.fileStorageService.moveFile(tmpFile, tarFile, mode);
    }

    /*************************************************定位型分片工具****************************************************/

    /**
     * 对分片列表按起始索引升序排序，并校验分片是否连续
     *
     * @param startIndex  开始索引
     * @param fileLength  文件长度
     * @param chunkBOList 分片信息
     */
    private void checkLocChunks(long startIndex, long fileLength, List<LocChunkStorage> chunkBOList) {
        // 对分片列表按起始索引升序排序
        CollectionUtil.sort(chunkBOList, (o1, o2) -> ObjectUtil.compare(o1.getStartBytes(), o2.getStartBytes()));
        String chunksInfo = JSONUtil.toJsonStr(chunkBOList);
        log.debug("分片信息校验：{}", chunksInfo);
        // 连续性校验
        for (LocChunkStorage chunkInfo : chunkBOList) {
            // 判断分片是否连续
            if (ObjectUtil.notEqual(chunkInfo.getStartBytes(), startIndex))
                throw new IORuntimeException("分片连续性异常：" + JSONUtil.toJsonStr(chunkInfo));
            // 设定下一个起始索引
            startIndex = chunkInfo.getEndBytes().intValue() + 1;
        }
        // 长度校验
        if (fileLength > 0 && ObjectUtil.notEqual(fileLength, startIndex - 1))
            throw new IORuntimeException(String.format("分片总长与文件长度不一致: fileLength: %s, chunksLength: %s",
                fileLength, startIndex - 1));
        log.debug("文件分片合并校验通过: {}", chunksInfo);
    }

    /*************************************************索引型分片工具****************************************************/

    private InxChunkMetadata checkInxChunks(FileStorage fileStorage, String chunkSuperPath, String fileId) {
        // 索引型分片元数据检查
        InxChunkMetadata chunkMetadata = this.readMetadata(chunkSuperPath, fileId);
        if (chunkMetadata == null)
            throw new IORuntimeException(new FileNotFoundException("分片操作元数据未找到"));
        if (StrUtil.isNotBlank(fileStorage.getFileName()))
            chunkMetadata.setFileName(fileStorage.getFileName());
        if (StrUtil.isNotBlank(fileStorage.getSuperPath()))
            chunkMetadata.setSuperPath(fileStorage.getSuperPath());
        if (fileStorage.getLength() != null)
            chunkMetadata.setLength(fileStorage.getLength());
        if (StrUtil.isNotBlank(fileStorage.getDigest()))
            chunkMetadata.setDigest(fileStorage.getDigest());
        if (StrUtil.isNotBlank(fileStorage.getDigestType()))
            chunkMetadata.setDigestType(fileStorage.getDigestType());
        if (StrUtil.isBlank(chunkMetadata.getFileName()))
            chunkMetadata.setFileName(chunkMetadata.getFileId());

        int totalChunks = chunkMetadata.getTotalChunks() == null ? 0 : chunkMetadata.getTotalChunks();
        if (totalChunks <= 0)
            throw new IORuntimeException("chunks metadata error，totalChunks is Less than or equal to 0!");
        int existChunks = this.countExistChunks(fileId, chunkSuperPath);
        if (existChunks != totalChunks)
            throw new IORuntimeException("file chunks is incomplete!");
        return chunkMetadata;
    }

    /**
     * 写入文件元数据
     * 文件不存在时自动创建
     *
     * @param chunkInfo 分片信息
     */
    private void writeMetadata(InxChunkStorage chunkInfo) {
        InxChunkMetadata chunkMetadata = this.readChunkMetadata(chunkInfo.getFileId(), chunkInfo.getSuperPath());
        if (chunkMetadata != null) {
            boolean unchanged = Objects.equals(chunkMetadata.getFileId(), chunkInfo.getFileId())
                && Objects.equals(chunkMetadata.getTotalChunks(), chunkInfo.getTotalChunks())
                && Objects.equals(chunkMetadata.getLength(), chunkInfo.getTotalSize());
            if (unchanged)
                return;
        }
        if (chunkMetadata == null)
            chunkMetadata = new InxChunkMetadata();
        chunkMetadata.setFileId(chunkInfo.getFileId());
        chunkMetadata.setTotalChunks(chunkInfo.getTotalChunks());
        chunkMetadata.setLength(chunkInfo.getTotalSize());
        chunkMetadata.setChunkSize(chunkInfo.getChunkSize());

        this.writeChunkMetadata(chunkMetadata, chunkInfo.getFileId(), chunkInfo.getSuperPath());
    }

    /**
     * 读取元数据
     *
     * @param superPath 父路径
     * @param fileId    文件唯一标识
     * @return 文件元数据字节数组，文件不存在时返回空数组
     */
    private InxChunkMetadata readMetadata(String superPath, String fileId) {
        return this.readChunkMetadata(fileId, superPath);
    }

    /**
     * 获取已上传的分片数量
     *
     * @param fileId
     * @param superPaths
     * @return
     */
    private int countExistChunks(String fileId, String... superPaths) {
        String pattern = "^(\\d+)$";
        File[] files = this.getChunkParent(fileId, superPaths).listFiles(file -> file.getName().trim().matches(pattern));
        return files == null ? 0 : files.length;
    }

}
